//===============================================================================
// Microsoft patterns & practices Enterprise Library Contribution
// Mapping Application Block
//===============================================================================

using System;
using System.Data;
using System.Reflection;
using System.Globalization;
using System.Collections.ObjectModel;
using EntLibContrib.Common;
using EntLibContrib.Mapping.Instrumentation;
using EntLibContrib.Mapping.Properties;

namespace EntLibContrib.Mapping.Mappers
{
	/// <summary>
	/// Abstract Mapper.
	/// </summary>
	/// <remarks>
	/// This provider is the base for all mappers
	/// </remarks>
	public abstract class MapperBase : IMapper
	{
		#region Constants
		private MapperType mapperType = MapperType.Custom;
		#endregion

		#region Fields
		private string name;
		private string tableName;
		private MapDictionary maps;
		private MappingInstrumentationProvider instrumentationProvider;
		#endregion

		#region Properties
		/// <summary>
		/// Gets the query name.
		/// </summary>
		/// <value>The name.</value>
		public string Name
		{
			get { return name; }
		}

		/// <summary>
		/// Gets the type of the mapper.
		/// </summary>
		/// <value>The type of the mapper.</value>
		public virtual MapperType MapperType
		{
			get { return mapperType; }
		}

		/// <summary>
		/// Gets the table name.
		/// </summary>
		/// <value>The table name.</value>
		public string TableName
		{
			get { return tableName; }
		}

		/// <summary>
		/// Gets the map dictionary.
		/// </summary>
		/// <value>A dictionary of maps.</value>
		public MapDictionary Maps
		{
			get { return maps; }
		}

		/// <summary>
		/// Gets or sets the instrumentation provider.
		/// </summary>
		/// <value>The instrumentation provider.</value>
		public MappingInstrumentationProvider InstrumentationProvider
		{
			get { return instrumentationProvider; }
		}
		#endregion

		#region Construction
		/// <summary>
		/// Initializes a new default instance of the <see cref="MapperBase"/> class.
		/// </summary>
		/// <param name="name">The mapper name.</param>
		/// <param name="tableName">The data transfer object table name.</param>
		/// <param name="maps">The maps.</param>
		/// <param name="instrumentationProvider">The instrumentation provider.</param>
		protected MapperBase(string name,
												string tableName,
												MapDictionary maps,
												MappingInstrumentationProvider instrumentationProvider)
		{
			if (string.IsNullOrEmpty(name))
				throw new ArgumentNullException("name");

			if (maps == null)
				throw new ArgumentNullException("maps");

			this.name = name;
			this.tableName = tableName;
			this.maps = maps;
			this.instrumentationProvider = instrumentationProvider;
		}
		#endregion

		#region Public Members
		/// <summary>
		/// Maps a data transfer object to a single domain object.
		/// </summary>
		/// <typeparam name="T">The domain object type</typeparam>
		/// <param name="dataTransferObject">The data transfer object.</param>
		/// <returns>A populated domain object</returns>
		/// <remarks>
		/// 	<para>If the table name is null then the first table in the DataSet is used.</para>
		/// 	<para>If there are multiple rows in the table then only the first row is considered.</para>
		/// </remarks>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
		public virtual T ToDomainObject<T>(DataSet dataTransferObject) where T : class
		{
			try
			{
				// ensure data transfer object is not null
				if (dataTransferObject == null)
					throw new ArgumentNullException("dataTransferObject");

				// get the data table
				DataTable table = ExtractTable(dataTransferObject, tableName);

				// check for table not found
				if (table == null)
					return null;

				// check for rows in the table
				if (table.Rows.Count == 0)
					return null;

				// create a new populated domain object
				var domainObject = CreateDomainObject<T>(table.Rows[0]);

				// update the mapping to counters
				UpdateMappingToCounters();

				return domainObject;
			}
			catch (Exception exception)
			{
				// log and rethrow the error
				LogMappingToError(exception);
				throw;
			}
		}

		/// <summary>
		/// Maps a data transfer object to a collection of domain objects.
		/// </summary>
		/// <typeparam name="T">The domain object type</typeparam>
		/// <param name="dataTransferObject">The data transfer object.</param>
		/// <returns>A collection of populated domain object</returns>
		/// <remarks>
		/// 	<para>If the table name is null then the first table in the DataSet is used.</para>
		/// 	<para>Each row in the table is mapped to a domain object.</para>
		/// </remarks>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
		public virtual Collection<T> ToDomainObjectCollection<T>(DataSet dataTransferObject) where T : class
		{
			try
			{
				// ensure data transfer object is not null
				if (dataTransferObject == null)
					throw new ArgumentNullException("dataTransferObject");

				// create empty domain object list
				Collection<T> domainObjectList = new Collection<T>();

				// get the data table
				DataTable table = ExtractTable(dataTransferObject, tableName);

				// check for table not found
				if (table == null)
					return domainObjectList;

				// create a new domain object for each row in the table
				foreach (DataRow row in table.Rows)
				{
					domainObjectList.Add(CreateDomainObject<T>(row));
				}

				// update the mapping to counters
				UpdateMappingToCounters();

				return domainObjectList;
			}
			catch (Exception exception)
			{
				// log and rethrow the error
				LogMappingToError(exception);
				throw;
			}
		}

		/// <summary>
		/// Maps a single domain object to a data transfer object.
		/// </summary>
		/// <typeparam name="T">The domain object type</typeparam>
		/// <param name="domainObject">The domain object.</param>
		/// <returns>A populated data transfer object</returns>
		/// <remarks>
		/// 	<para>If the table name is null then the table in the DataSet will be called the same as the domain object.</para>
		/// 	<para>The table will contain just one row.</para>
		/// </remarks>
		public virtual DataSet FromDomainObject<T>(T domainObject) where T : class
		{
			try
			{
				if (domainObject == null)
					throw new ArgumentNullException("domainObject");

				// initialise the data transfer object. This creates a dataset and adds a table
				DataSet dataTransferObject = InitializeDataTransferObject();
				DataTable dataTransferTable = dataTransferObject.Tables[0];

				// initialise the data transfer table columns.
				InitializeDataTransferObjectColumns<T>(domainObject, dataTransferTable);

				// add a single row to the data transfer object table and fill it with data from the domain object
				FillDataTransferObjectRow<T>(domainObject, dataTransferTable);

				// update the mapping from counters
				UpdateMappingFromCounters();

				return dataTransferObject;
			}
			catch (Exception exception)
			{
				// log and rethrow the error
				LogMappingFromError(exception);
				throw;
			}
		}

		/// <summary>
		/// Maps a collection of domain objects to a data transfer object.
		/// </summary>
		/// <typeparam name="T">The domain object type</typeparam>
		/// <param name="domainObjectCollection">The domain object collection.</param>
		/// <returns>A populated data transfer object</returns>
		/// <remarks>
		/// 	<para>If the table name is null then the table in the DataSet will be called the same as the domain object.</para>
		/// 	<para>The table will contain a row for each domain object in the collection.</para>
		/// 	<para>The domain object collection must contain just one object type or supertype set.</para>
		/// </remarks>
		public virtual DataSet FromDomainObjectCollection<T>(Collection<T> domainObjectCollection) where T : class
		{
			try
			{
				if (domainObjectCollection == null)
					throw new ArgumentNullException("domainObjectCollection");

				if (domainObjectCollection.Count == 0)
					throw new ArgumentException(InternalResources.ExceptionDomainObjectCollectionEmpty, "domainObjectCollection");

				// initialise the data transfer object based on the first object
				DataSet dataTransferObject = InitializeDataTransferObject();
				DataTable dataTransferTable = dataTransferObject.Tables[0];

				// initialise the type cache
				Collection<string> typeCache = new Collection<string>();

				// initialise the data transfer table columns.
				foreach (T domainObject in domainObjectCollection)
				{
					if (!typeCache.Contains(domainObject.GetType().FullName))
					{
						typeCache.Add(domainObject.GetType().FullName);
						InitializeDataTransferObjectColumns<T>(domainObject, dataTransferTable);
					}
				}

				// add a row and fill with data for each domain object in the collection
				foreach (T domainObject in domainObjectCollection)
				{
					FillDataTransferObjectRow<T>(domainObject, dataTransferObject.Tables[0]);
				}

				// update the mapping from counters
				UpdateMappingFromCounters();

				return dataTransferObject;
			}
			catch (Exception exception)
			{
				// log and rethrow the error
				LogMappingFromError(exception);
				throw;
			}
		}
		#endregion

		#region Protected Members
		/// <summary>
		/// Creates a new populated domain object.
		/// </summary>
		/// <typeparam name="T">The domain object type</typeparam>
		/// <param name="dataTransferTableRow">The data transfer table row containing the domain object property values.</param>
		/// <returns>a populated domain object.</returns>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
		protected abstract T CreateDomainObject<T>(DataRow dataTransferTableRow) where T : class;

		/// <summary>
		/// Initialises the data transfer object.
		/// </summary>
		/// <returns>A DataSet with a single named table</returns>
		protected virtual DataSet InitializeDataTransferObject()
		{
			DataSet dataTransferObject = new DataSet();
			dataTransferObject.Locale = CultureInfo.CurrentCulture;
			if (string.IsNullOrEmpty(tableName))
				dataTransferObject.Tables.Add();
			else
				dataTransferObject.Tables.Add(tableName);

			return dataTransferObject;
		}

		/// <summary>
		/// Initialises the data transfer object columns.
		/// </summary>
		/// <typeparam name="T">The domain object type</typeparam>
		/// <param name="domainObject">The domain object.</param>
		/// <param name="dataTransferTable">The data transfer table.</param>
		/// <remarks>The columns for the data transfer table are derived from the domain object properties and the configured mappings</remarks>
		protected virtual void InitializeDataTransferObjectColumns<T>(T domainObject, DataTable dataTransferTable) where T : class
		{
			// generate the columns
			foreach (PropertyInfo domainObjectProperty in ExtractProperties<T>(domainObject))
			{
				if (maps.ContainsKey(domainObjectProperty.Name))
				{
					if (string.IsNullOrEmpty(maps[domainObjectProperty.Name].ColumnName))
					{
						if (!dataTransferTable.Columns.Contains(domainObjectProperty.Name))
							dataTransferTable.Columns.Add(domainObjectProperty.Name, domainObjectProperty.PropertyType);
					}
					else
					{
						if (!dataTransferTable.Columns.Contains(maps[domainObjectProperty.Name].ColumnName))
							dataTransferTable.Columns.Add(maps[domainObjectProperty.Name].ColumnName, domainObjectProperty.PropertyType);
					}
				}
			}
		}

		/// <summary>
		/// Creates a single data transfer object row and fills it with data.
		/// </summary>
		/// <typeparam name="T">The domain object type</typeparam>
		/// <param name="domainObject">The domain object.</param>
		/// <param name="dataTransferTable">The initialised data transfer table.</param>
		/// <remarks>The table row data is derived from the domain object properties</remarks>
		protected virtual void FillDataTransferObjectRow<T>(T domainObject, DataTable dataTransferTable) where T : class
		{
			// generate a new data transfer table row
			DataRow dataTransferTableRow = dataTransferTable.NewRow();
			// create a nullable writer for the DataRow
			INullableWriter<DataRow> nullableDataRowWriter = new NullableDataRowWriter(dataTransferTableRow);
			foreach (PropertyInfo domainObjectProperty in ExtractProperties<T>(domainObject))
			{
				if (maps.ContainsKey(domainObjectProperty.Name))
				{
					if (string.IsNullOrEmpty(maps[domainObjectProperty.Name].ColumnName))
						nullableDataRowWriter.SetValue(domainObjectProperty.Name, domainObjectProperty.GetValue(domainObject, null), domainObjectProperty.PropertyType);
					else
						nullableDataRowWriter.SetValue(maps[domainObjectProperty.Name].ColumnName, domainObjectProperty.GetValue(domainObject, null), domainObjectProperty.PropertyType);
				}
			}

			// add the new row to the data transfer object table
			dataTransferTable.Rows.Add(dataTransferTableRow);
		}

		/// <summary>
		/// Fills a domain object with data from a data transfer table row.
		/// </summary>
		/// <typeparam name="T">The domain object type</typeparam>
		/// <param name="domainObject">The domain object.</param>
		/// <param name="dataTransferTableRow">The data transfer table row.</param>
		/// <remarks>The domain object properties are set from the table row data based on configured mapping</remarks>
		protected virtual void FillDomainObject<T>(T domainObject, DataRow dataTransferTableRow) where T : class
		{
			// create a nullable reader for the DataRow
			INullableReader nullableDataRowReader = new NullableDataRowReader(dataTransferTableRow);
			//for each mapping populate the domain object property
			foreach (PropertyInfo domainObjectProperty in ExtractProperties<T>(domainObject))
			{
				// determine if the maps contain the domain object property
				if (maps.ContainsKey(domainObjectProperty.Name))
				{
					// determine if the data transfer row is using the property name or a given column name
					if (string.IsNullOrEmpty(maps[domainObjectProperty.Name].ColumnName))
					{
						// determine if the data transfer row contains the mapped column name
						if (nullableDataRowReader.IsField(domainObjectProperty.Name))
							domainObjectProperty.SetValue(domainObject, nullableDataRowReader.GetNullableValue(domainObjectProperty.Name, domainObjectProperty.PropertyType), null);
					}
					else
					{
						// determine if the data transfer row contains the mapped property name
						if (nullableDataRowReader.IsField(Maps[domainObjectProperty.Name].ColumnName))
							domainObjectProperty.SetValue(domainObject, nullableDataRowReader.GetNullableValue(Maps[domainObjectProperty.Name].ColumnName, domainObjectProperty.PropertyType), null);
					}
				}
			}
		}

		/// <summary>
		/// Updates the MappingTo counters.
		/// </summary>
		protected virtual void UpdateMappingToCounters()
		{
			if (instrumentationProvider != null)
			{
				instrumentationProvider.FireMappingTo(MapperType);
			}
		}

		/// <summary>
		/// Updates the MappingFrom counters.
		/// </summary>
		protected virtual void UpdateMappingFromCounters()
		{
			if (instrumentationProvider != null)
			{
				instrumentationProvider.FireMappingFrom(MapperType);
			}
		}

		/// <summary>
		/// Log a mapping to error.
		/// </summary>
		protected virtual void LogMappingToError(Exception exception)
		{
			// log the error
			if (instrumentationProvider != null)
				instrumentationProvider.FireMapperFailed(InternalResources.ErrorMappingToFailedMessage, exception);
		}

		/// <summary>
		/// Log a mapping from error.
		/// </summary>
		protected virtual void LogMappingFromError(Exception exception)
		{
			// log the error
			if (instrumentationProvider != null)
				instrumentationProvider.FireMapperFailed(InternalResources.ErrorMappingFromFailedMessage, exception);
		}
		#endregion

		#region Private Members
		/// <summary>
		/// Extracts a table from the data transfer object.
		/// </summary>
		/// <param name="dataTransferObject">The data transfer object.</param>
		/// <param name="tableName">Name of the table to be extracted.</param>
		/// <returns>A single data table</returns>
		private static DataTable ExtractTable(DataSet dataTransferObject, string tableName)
		{
			// check for empty dataset
			if (dataTransferObject.Tables.Count == 0)
				return null;

			// get table
			DataTable table = null;
			if (string.IsNullOrEmpty(tableName))
				table = dataTransferObject.Tables[0];
			else
				if (dataTransferObject.Tables.Contains(tableName))
					table = dataTransferObject.Tables[tableName];

			return table;
		}

		/// <summary>
		/// Extracts the properties from a domain object type.
		/// </summary>
		/// <typeparam name="T">The domain object type</typeparam>
		/// <param name="domainObject">The domain object.</param>
		/// <returns>a collection of property information</returns>
		private static PropertyInfo[] ExtractProperties<T>(T domainObject) where T : class
		{
			// get the domain object properties
			return domainObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
		}
		#endregion
	}
}
